太忙了,哪有那個美國時間寫test啦!寫test的時間,都可以寫兩個function了。
這次的project沒很難呀,把以前寫過的兜一兜就好了,不會出錯啦!
呵呵,類似的話,是不是聽起來很熟悉呀?
說實話,我們也曾經是不太喜歡寫test
的一群,尤其有時候要mock
來mock
去的時候,很容易耗盡腦力,還不如拿來寫code勒。
但隨著日漸肥大的code,以及參與project的兄弟越來越多時,不寫test
可謂寸步難行呀。code裡一堆潛藏的bug,也沒辦法CI/CD
,到最後反而得花更多時間。
所以我們試圖找到一個平衡點,盡量先寫剛剛好夠用的test
,之後有時間再慢慢增加。
針對box drop project,我們也是秉持著剛剛好的原則,可能不會每個function
都寫test
,但希望大致的脈絡都有被檢測到。
或許諸位會覺得這個project到現在應該沒什麼錯吧?其實我們藏了一個對ANSA來說不算錯的小細節來與各位分享(註1
)。
這個問題真是戳中我們的痛處啊,我們也是pytest
的愛好者啊!
但是一來pytest
還不在ANSA附帶的third-party package
內,如果客戶端環境沒網路又不能插USB,怎麼裝pytest
呀?怎麼收集test
的資訊呢?
二來,我們還沒找到辦法可以順利的讓pytest
執行。我們試了很多方法,像是sys.path.append
、subprocess
、從command line呼叫,甚至整個pytest
搬進site-packages
等,就是沒辦法work
(崩潰)。
所以截至目前為止,我們還是使用Python內建的unittest
來寫test
。如果諸位有更好的處理方法,衷心希望能跟我們分享,感激不盡。
於TestBoxDropBase
中建立_setup
及_tear_down
,並分別於setUp
及tearDown
中呼叫。每個test function
執行前會呼叫setUp
,執行後則會呼叫tearDown
。
我們這樣安排的目的,是希望TestBoxDropBase
可以被後續的Test class
所繼承。如果該Test class
有需要特別的setUp
或tearDown
,可以overwrite_setup
或_tear_down
。
_setup
目前沒有需要設定的部份。_tear_down
收集ANSA內所有的Entity
,並刪除。# tests.py
class TestBoxDropBase(unittest.TestCase):
deck = constants.LSDYNA
def setUp(self):
self._setup()
def tearDown(self):
self._tear_down()
def _setup(self):
pass
def _tear_down(self):
ents = base.CollectEntities(self.deck, None, LSDYNAType.ALL)
base.DeleteEntity(ents)
TestCreators
我們測試了create_mat
、create_prop
、create_set
及create_contact
四個function
,下面我們以create_mat
為例來說明。
與create_mat
有關的test function
有test_create_mat
、test_create_mat_given_name
及test_create_mat_given_vals
三個。
test_create_mat
:
create_mat
建立Entity
,參數都使用預設值。Entity
_id
是否為1
,及_name
內是否含有auto
字串。test_create_mat_given_name
:
name
來呼叫create_mat
建立Entity
。Entity
的_name
是否與給定的一樣,及其內應該是不含有auto
字串。test_create_mat_given_vals
:
E
,並置入vals
中來呼叫create_mat
建立Entity
。Entity
card values
中的E
是否與給定的一樣。#tests.py
class TestCreators(TestBoxDropBase):
def test_create_mat(self):
mat = create_mat()
mat_id, mat_name = mat._id, mat._name
self.assertEqual(mat_id, 1)
self.assertIn('auto', mat_name)
def test_create_mat_given_name(self):
given_mat_name = 'dummy_mat_name'
mat = create_mat(name=given_mat_name)
mat_name = mat._name
self.assertEqual(given_mat_name, mat_name)
self.assertNotIn('auto', mat_name)
def test_create_mat_given_vals(self):
e = 123456
vals = {'E': e}
mat = create_mat(vals=vals)
e_value = mat.get_entity_values(self.deck, ['E'])['E']
self.assertEqual(e, e_value)
def test_create_prop(self):
prop = create_sec()
prop_id, prop_name = prop._id, prop._name
self.assertEqual(prop_id, 1)
self.assertIn('auto', prop_name)
mat = prop.get_entity_values(self.deck, ['MID'])['MID']
mat_id, mat_name = mat._id, mat._name
self.assertEqual(mat_id, 1)
self.assertIn('auto', mat_name)
def test_create_prop_given_name(self):
given_prop_name = 'dummy_prop_name'
prop = create_sec(name=given_prop_name)
prop_name = prop._name
self.assertEqual(given_prop_name, prop_name)
self.assertNotIn('auto', prop_name)
def test_create_prop_given_vals(self):
t1 = 2
vals = {'T1': t1}
prop = create_sec(vals=vals)
t1_value = prop.get_entity_values(self.deck, ['T1'])['T1']
self.assertEqual(t1, t1_value)
def test_create_set(self):
set_ = create_set()
set_id, set_name = set_._id, set_._name
self.assertEqual(set_id, 1)
self.assertIn('auto', set_name)
def test_create_set_given_name(self):
given_set_name = 'dummy_set_name'
set_ = create_set(name=given_set_name)
set_name = set_._name
self.assertEqual(given_set_name, set_name)
self.assertNotIn('auto', set_name)
def test_create_set_add_prop(self):
prop = create_sec()
set_ = create_set(prop)
prop_ents = base.CollectEntities(self.deck, set_, LSDYNAType.PROPERTY)
self.assertEqual(len(prop_ents), 1)
all_ents = base.CollectEntities(self.deck, set_, LSDYNAType.ALL)
self.assertEqual(len(all_ents), 1)
def test_create_contact(self):
set1 = create_set()
set2 = create_set()
contact = create_contact(ssid=set1._id,
msid=set2._id,
sstyp=ContactType.TYPE2_PART_SET.value,
mstyp=ContactType.TYPE2_PART_SET.value)
contact_id, contact_name = contact._id, contact._name
self.assertEqual(contact_id, 1)
self.assertIn('auto', contact_name)
TestID
我們測試了test_get_mat_prop_id
、test_get_id
、test_mix_id
、test_get_fit_id_range
及test_get_fit_mix_id_range
五個function
。
test_get_mat_prop_id
:
create_sec
。create_mat
。create_sec
會同時建立material
及property
,此時ANSA裡應該有十個property
及十一個material
。get_mat_prop_id
是否能自動傳回下一個material
及property
同時可用的id=12
。test_get_id
:
create_set
。get_id
是否回傳下一個可用id=11
。test_get_id
:
create_sec
。create_set
。get_mix_id
是否回傳下一個property
及set
同時可用id=11
。test_get_fit_id_range
:
create_mat
。MID=10
的create_mat
。material
,id
分別為1
及10
。id
時,get_fit_id_range
能否回傳一個當下可用的range,即2
及10
,中間能夠使用的為2
~9
。test_get_fit_mix_id_range
:
create_mat
。MID=10
的create_mat
。create_sec
。create_set
。material
(id
分別為1
、2
、10
)、一個property
(id
為1
)及十一個set
(id
為1
~11
)。id
時,test_get_fit_mix_id_range
能否回傳一個當下可用的mix range,即12
及20
,中間能夠使用的為12
~19
。# tests.py
class TestID(TestBoxDropBase):
def test_get_mat_prop_id(self):
for _ in range(10):
create_sec()
create_mat()
mat_prop_id = get_mat_prop_id()
self.assertEqual(mat_prop_id, 12)
def test_get_id(self):
for _ in range(10):
create_set()
set_id = get_id(LSDYNAType.SET)
self.assertEqual(set_id, 11)
def test_mix_id(self):
for _ in range(5):
create_sec()
for _ in range(10):
create_set()
mix_id = get_mix_id([LSDYNAType.PROPERTY, LSDYNAType.SET])
self.assertEqual(mix_id, 11)
def test_get_fit_id_range(self):
create_mat()
create_mat(vals={'MID': 10})
start, end_ = get_fit_id_range(8, LSDYNAType.MATERIAL)
self.assertEqual(start, 2)
self.assertEqual(end_, 10)
def test_get_fit_mix_id_range(self):
create_mat()
create_mat(vals={'MID': 10})
create_sec()
for _ in range(11):
create_set()
start, end_ = get_fit_mix_id_range(8, [LSDYNAType.MATERIAL,
LSDYNAType.PROPERTY,
LSDYNAType.SET])
self.assertEqual(start, 12)
self.assertEqual(end_, 20)
TestMisc
我們測試了test_create_boundary_spc
、test_create_boundary_spc_c_str
、test_create_initial_velocity
、test_create_ctrl_card
、test_create_ctrl_card
及test_output_file
六個function
。
test_create_boundary_spc
:
create_set
,建立一個set Entity
。create_boundary_spc
,並指定fields
為上一步建立的set
及c=123456
,來建立一個boundary_spc_set Entity
。Entity
的NSID
之_id
及c
是否與給定值相同。test_create_boundary_spc_c_str
:
test_create_boundary_spc
相同,但此處c = '123456'
為str
型態。Entity
的NSID
之_id
是否與給定值相同。Entity
的c
是否與給定值不同。test_create_initial_velocity
:
create_set
,建立一個set Entity
。create_boundary_spc
,並指定fields
為上一步建立的set
及VZ=-500
,來建立一個initial_velocity_set Entity
。Entity
的NSID
之_id
及VZ
是否與給定值相同。test_create_ctrl_card
:
get_card_ent
,得到一個control Entity
。card_handler
,並指定參數為上一步所得的control Entity
及TERMINATION
的ENDTIM
為1.5E-2
。Control Entity
的TERMINATION
是否為ON
及TERMINATION_ENDTIM
是否與給定值相同。test_create_db_card
:
get_card_ent
,得到一個database Entity
。card_handler
,並指定參數為上一步所得的database Entity
及D3PLOT
的DT
為2E-4
。database Entity
的D3PLOT
是否為ON
及D3PLOT_DT
是否與給定值相同。test_output_file
:
filename
建立一個pathlib.Path object
。output_file
寫出k檔。Path.is_file
檢查k檔是否在硬碟中。context manager
:
ANSA
字串是否在內(註2
)。ANSA
字串,則raise AssertionError
。assert
成功與否,皆呼叫Path.unlink()
刪除k檔。# tests.py
class TestMisc(TestBoxDropBase):
def test_create_boundary_spc(self):
c = 123456
set_ = create_set()
set_id = set_._id
fields = ('NSID', 'c')
boundary_spc = create_boundary_spc(dict(zip(fields, (set_id, c))))
card_values = boundary_spc.get_entity_values(self.deck, fields)
spc_nsid, spc_c = card_values['NSID']._id, card_values['c']
self.assertEqual(spc_nsid, set_id)
self.assertEqual(spc_c, c)
def test_create_boundary_spc_c_str(self):
c = '123456'
set_ = create_set()
set_id = set_._id
fields = ('NSID', 'c')
boundary_spc = create_boundary_spc(dict(zip(fields, (set_id, c))))
card_values = boundary_spc.get_entity_values(self.deck, fields)
spc_nsid, spc_c = card_values['NSID']._id, card_values['c']
self.assertEqual(spc_nsid, set_id)
self.assertNotEqual(spc_c, c)
def test_create_initial_velocity(self):
vz = -500
box_set = create_set()
box_set_id = box_set._id
fields = ('NSID', 'VZ')
initial_velocity = create_initial_velocity(
dict(zip(fields, (box_set_id, vz))))
card_values = initial_velocity.get_entity_values(self.deck, fields)
iv_nsid, iv_vz = card_values['NSID']._id, card_values['VZ']
self.assertEqual(iv_nsid, box_set_id)
self.assertEqual(iv_vz, vz)
def test_create_ctrl_card(self):
endtim = 1.5E-2
crtl_ent = get_card_ent(ControlCardType.CONTROL)
ctrl_params = [('TERMINATION', {'ENDTIM': endtim})]
crtl_ent = card_handler(crtl_ent, ctrl_params)
fields = ('TERMINATION', 'TERMINATION_ENDTIM')
card_values = crtl_ent.get_entity_values(self.deck, fields)
termination, termination_endtim = card_values.values()
self.assertEqual(termination, 'ON')
self.assertEqual(termination_endtim, endtim)
def test_create_db_card(self):
dt = 2E-4
db_ent = get_card_ent(ControlCardType.DATABASE)
db_params = [('D3PLOT', {'DT': dt})]
db_ent = card_handler(db_ent, db_params)
fields = ('D3PLOT', 'D3PLOT_DT')
card_values = db_ent.get_entity_values(self.deck, fields)
db, db_d3plot = card_values.values()
self.assertEqual(db, 'ON')
self.assertEqual(db_d3plot, dt)
def test_output_file(self):
filename = 'lsdyna_test_file.k'
p = Path(output_file(filename, self.deck))
self.assertTrue(p.is_file())
with open(p) as f:
try:
self.assertIn('ANSA', f.read(30))
except AssertionError as ex:
raise ex
finally:
p.unlink()
_setup
,將產生plate
所需參數於此設定。test_create_plate_v1
:
create_sec
建立一個material
及property
,命名其id
為 plate_mat_prop_id
。create_set
,並將剛剛建立的property Entity
置入其中。MyImportV1
建立一個context manager
,命名為plate_import_v1
。plate_import_v1
範圍內呼叫plate_import_v1
。node
及shell
數量是否正確。# tests.py
class TestPlate(TestBoxDropBase):
def _setup(self):
self.l, self.w, self.en1, self.en2 = 100, 100, 10, 10
self.z_elv, self.move_xy, self.rot_angle = 0, None, None
def test_create_plate_v1(self):
plate_prop = create_sec()
plate_mat_prop_id = plate_prop._id
plate_set = create_set(plate_prop, 'plate', deck=self.deck)
plate_import_v1 = MyImportV1()
with plate_import_v1 as import_v1:
create_plate_v1(import_v1,
self.l,
self.w,
self.en1,
self.en2,
plate_mat_prop_id,
z_elv=self.z_elv,
move_xy=self.move_xy,
rot_angle=self.rot_angle,
deck=self.deck)
n_nodes = (self.en1+1)*(self.en2+1)
n_shells = self.en1*self.en2
self.assertEqual(len(plate_import_v1.nodes), n_nodes)
self.assertEqual(len(plate_import_v1.shells), n_shells)
_setup
,將產生box
所需參數於此設定。test_create_box_v1
:
create_sec
建立一個material
及property
,命名其id
為 box_mat_prop_id
。create_set
,並將剛剛建立的property Entity
置入其中。MyImportV1
建立一個context manager
,命名為box_import_v1
。box_import_v1
範圍內呼叫create_box_v1
。node
及solid
數量是否正確。# tests.py
class TestBox(TestBoxDropBase):
def _setup(self):
self.l, self.w, self.h, self.en1, self.en2, self.en3 = 50, 50, 50, 10, 10, 10
self.z_elv, self.move_xy, self.rot_angle = 5, (50, 20), 45
def test_create_box_v1(self):
box_prop = create_sec()
box_mat_prop_id = box_prop._id
box_set = create_set(box_prop, 'box', deck=self.deck)
box_import_v1 = MyImportV1()
with box_import_v1 as import_v1:
create_box_v1(import_v1,
self.l,
self.w,
self.h,
self.en1,
self.en2,
self.en3,
box_mat_prop_id,
z_elv=self.z_elv,
move_xy=self.move_xy,
rot_angle=self.rot_angle,
deck=self.deck)
n_nodes = (self.en1+1)*(self.en2+1)*(self.en3+1)
n_solids = self.en1*self.en2*self.en3
self.assertEqual(len(box_import_v1.nodes), n_nodes)
self.assertEqual(len(box_import_v1.solids), n_solids)
我們測試的方法是透過搭配Python的unittest.main
及ANSA的command line interface
於terminal
中執行。
於tests.py
的最後加入下面幾行:
# tests.py
if __name__ == '__main__'
unittest.main()
接著在terminal
中輸入:
ANSA路徑 -execscript "tests.py路徑" -nogui
輸入指令會像:
~/BETA_CAE_Systems/ansa_v23.0.0/ansa64.sh -execscript "./tests.py" -nogui
其中-nogui
可以讓我們於不啟動ANSA GUI的情況下使用ANSA。
註1:於create_boundary_spc
中的c
其實應該是int
型態,但如果給str
型態,ANSA也是可以接受,不會報錯。這個問題,也是我們在寫test
時發現的,本來想直接在[Day06]~[Day07]的box_drop.py
裡就修正,但轉念一想,或許直接分享在今天的內容,也會是不錯的學習體驗。
註2:output_file
格式大概會像:
$
$ANSA_VERSION;23.0.0;
$
$
$ file created by A N S A Wed Sep 01 11:34:15 2022
$
$ output from :
$
$
$
$ Settings :
$
$ Output format : R13
$
$ Output : Visible
$
$
$
$
*KEYWORD
*END
[Day17]我們建立了一個名為_create_entity
的helper function
,用以幫助大部分的creator
呼叫base.CreateEntity
。相關修改可以參考creators.py
。
#creators.py
def _create_entity(type_, fields, deck=None):
deck = deck or constants.LSDYNA
return base.CreateEntity(deck, type_, fields)